Published on
·

React Query Key 효과적으로 관리하기

React Query는 React 프로젝트에서 서버 상태 관리를 용이하게 해주는 강력한 라이브러리로 잘 알려져있습니다. 그 중 Query Key(이하 쿼리 키)는 React Query에서 매우 중요한 개념입니다. 이 글은 쿼리 키의 특성과 어떻게 하면 이를 효과적으로 관리할 수 있는지, 그리고 실제 프로젝트에 적용시킨 사례에 대해 다룹니다.

캐싱 데이터

React Query는 같은 쿼리 키에 대해 다시 조회할 때 캐싱된 값을 받습니다. 이 쿼리 캐시는 직렬화된 key, value 쌍 자바스크립트 객체입니다. 그래서 우리는 이 캐시값을 가져오기 위해 쿼리 키를 사용하는 것이고요. 키는 결정적인(deterministically) 방식으로 해싱되기 때문에 객체를 사용할 수도 있습니다.

useQuery({ queryKey: ['todos', { status, page }], ... })
useQuery({ queryKey: ['todos', { page, status }], ...})
useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... })

가장 중요한 것은 캐시에서 키에 대한 항목을 바로 찾기 위해, 쿼리는 고유한 키를 가져야 한다는 것입니다. 당연히 useQueryuseInfiniteQuery에 동일한 키를 사용할 수 없습니다. 하나의 쿼리 캐시를 공유하게 될테니까 말이죠.

선언적 쿼리

쿼리와 재조회를 명령적인 방식으로 오해하기 쉽지만, 사실 선언적입니다. 쿼리는 자동으로 재조회됩니다. 우리가 할 일은 데이터를 다시 가져와야 하는 어떤 상태가 있다면 그것을 쿼리 키에 포함시키는 것 뿐입니다.

// 🚨 재조회를 직접 명령하는 것은 불가능합니다.
function Component() {
  const { data, refetch } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  return <Filters onApply={() => refetch(???)} />
}

// ✅ 쿼리 키에 포함시켜 선언하면 자동으로 재조회합니다.
function Component() {
  const [filters, setFilters] = React.useState()
  const { data } = useQuery({
    queryKey: ['todos', filters],
    queryFn: () => fetchTodos(filters),
  })

  return <Filters onApply={setFilters} />
}

쿼리 키의 관리

이런 쿼리 키를 매번 수동으로 선언할 경우 오류가 발생하기 쉽고, 중복되기 쉬우며, 일관성이 없어집니다.

  • shiftListshitfList로 작성하면 캐싱 되지 않습니다.
  • 이미 다른 곳에서 같은 목적의 쿼리 키를 만들었다면 과거의 쿼리 키를 찾아야 합니다.
  • 이미 다른 곳에서 같은 목적의 쿼리 키를 만들었지만, 그것을 잊은채로 같은 쿼리를 다른 이름으로 명명할 수도 있습니다.
  • 상수로 관리하기엔 키에 또 다른 세부 수준을 추가하기 어렵습니다.

쿼리 키 팩토리

각 기능이나 관심사마다 하나의 쿼리 키 팩토리를 만들어서 이를 해결할 수 있습니다. 쿼리 키 팩토리는 단순한 객체로, 메서드를 통해 쿼리 키를 생성할 수 있습니다.

기본 구조 만들기

저희 프로젝트에서 ward에 관련된 쿼리 키를 쿼리 키 팩토리를 통해 관리하는 기본 구조를 만들어보겠습니다.

export const wardKeys = {
  base: ['ward'] as const,
  requestList: (wardId: number, teamId: number, year: number, month: number) =>
    [...wardKeys.base, 'requests', wardId, teamId, year, month] as const,
  shiftList: (wardId: number, teamId: number, year: number, month: number) =>
    [...wardKeys.base, 'shifts', wardId, teamId, year, month] as const,
  linkedMembers: (wardId: number, teamId: number) =>
    [...wardKeys.base, 'linked', wardId, teamId] as const,
};

매개변수 객체로 변경하기

추후에 매개변수가 늘어날 가능성을 고려하여, 객체 형태로 매개변수를 받는 것을 고려할 수 있습니다.

requestList: ({ wardId, teamId, year, month }: { wardId: number; teamId: number; year: number; month: number; }) =>
  [...wardKeys.base, 'requests', wardId, teamId, year, month] as const,

이렇게 하면, 함수 호출 시에 매개변수의 순서를 신경 쓸 필요가 없어지고, 추후에 매개변수가 추가되거나 변경될 때 더 유연하게 대응할 수 있습니다.

JSDoc으로 queryFn 알려주기

조금만 더 신경써서, 쿼리 키 마다 JSDoc을 사용해서 어떤 queryFn과 매칭되는지 알려주기로 했습니다. 재사용할 때 해당 쿼리 키가 어떤 역할을 하는지 좀 더 분명해집니다.

/** getWardRequestList */
requestList: ({ wardId, teamId, year, month }: { wardId: number; teamId: number; year: number; month: number; }) =>
  [...wardKeys.base, 'requests', wardId, teamId, year, month] as const,

쿼리 키 사용하기

const { data, isLoading } = useQuery(
    wardKeys.requestList({wardId, teamId, year, month}),
    () => getWardShiftRequest({wardId, teamId, year, month})
  );

쿼리 키 팩토리 방식의 장점

  • 쿼리 키를 중복 생성할 가능성을 예방합니다.
  • 쿼리 키를 사용할 때 IDE에서 자동완성을 지원해주어, 오류를 방지합니다.
  • 기존에 생성했던 쿼리 키를 찾기 위해 시간을 사용할 필요가 없습니다.
  • 일정한 규칙을 따르고 있기 때문에 확장이 편합니다.
  • 매개변수 타입을 명확이 정하여 타입 안정성을 보장합니다.

모든 상황에서 쿼리 키 팩토리를 써야하는 것은 아닙니다. 프로젝트 규모가 작거나, 이미 쿼리 키를 효과적으로 관리하고 있다면 굳이 쿼리 키 팩토리를 도입할 필요는 없습니다. 이 방법은 쿼리 키를 관리하는 여러 방법 중 React Query 라이브러리의 Maintainer TkDodo가 추천하는 방법입니다. 매개변수를 처리하는 방식이나 JSDoc을 사용하는 방식은 이것을 저의 프로젝트에 적용하면서 추가한 방식입니다.